
#version 330 core
in vec3 dir;
out vec3 color;

uniform float time;
uniform float gun_force;
uniform mat3 gun_axis;
uniform vec3 camera_pos;
uniform vec3 player_pos;
uniform vec3 bullet_pos;
uniform float bullet_age;
uniform float bullet_size;

const float small = 5e-3;
const float large = 1e+3;
vec3 ndir = normalize(dir);

vec3 sky_color;
vec3 sun_color;
vec3 sky_diffuse;
float sun_size;
uniform vec3 sun_dir;
#define SKY_COLOR				(vec3(1, 1.1, 1.4)*0.21)
#define SKY_TWILIGHT_COLOR		(vec3(1, 0.6, 0.2)*0.25)
#define SKY_NIGHT_COLOR			(vec3(0.6, 0.5, 0.7)*0.3)
#define SUN_COLOR				(vec3(1.4, 1.2, 1)*0.4)
float night_to_dawn = (clamp(sun_dir.y, -0.3, 0.2) + 0.3) / 0.5;



float lerp(float x, float f, float t) { return (t-f)*x + f; }
float lerp(float x, float xf, float xt, float f, float t) { return (x-xf) / (xt-xf) * (t-f) + f; }




//------ shape
float sphere(vec3 pos, float radius) { return length(pos) - radius; }
float ground(vec3 pos, float y) { return pos.y - y; }
float ubox(vec3 pos, vec3 size) { return length(max(abs(pos) - size, 0)); }
float cylinder(vec3 pos, float radius, float len)
{
	vec2 d = abs(vec2(length(pos.xy), pos.z)) - vec2(radius, len);
	return min(max(d.x, d.y), 0) + length(max(d, 0));
}

#define TANK_SIZE 0.25
float tank(vec3 pos)
{
	float gun_len = TANK_SIZE * lerp(gun_force, 1.618, 1);
	float gun_radius = TANK_SIZE * lerp(gun_force, 0.118, 0.15);
	pos -= vec3(0, -TANK_SIZE, 0);
	float base =   sphere(pos, TANK_SIZE);
	float head =   sphere(pos - vec3(0, TANK_SIZE, 0), TANK_SIZE * 0.618);
	float  gun = cylinder(inverse(gun_axis) * (pos - vec3(0, TANK_SIZE * 1.1, 0)) - vec3(0, 0, -TANK_SIZE * 1.1), gun_radius, gun_len);
	return min(min(base, head), gun);
}



//------ object
struct object
{
	int id;
	float dist;
};
object objects(object a, object b) { return (a.dist < b.dist ? a : b); }
object objects(object a, object b, object c) { return objects(a, objects(b, c)); }
object objects(object a, object b, object c, object d) { return objects(a, objects(b, c, d)); }
object objects(object a, object b, object c, object d, object e) { return objects(a, objects(b, c, d, e)); }

// object identifiers
#define OBJECT_SKY				0
#define OBJECT_GROUND			1
#define OBJECT_JADE				2
#define OBJECT_PLASTIC			3
#define OBJECT_PLAYER			4
#define OBJECT_BULLET           5

object obj_sky    (        ) { return object(OBJECT_SKY    , large               ); }
object obj_ground (vec3 pos) { return object(OBJECT_GROUND , ground(pos,     -1 )); }
object obj_jade   (vec3 pos) { return object(OBJECT_JADE   , sphere(pos,      1 )); }
object obj_plastic(vec3 pos) { return object(OBJECT_PLASTIC,   ubox(pos, vec3(1))); }
object obj_player (vec3 pos) { return object(OBJECT_PLAYER ,   tank(pos         )); }
object obj_bullet (vec3 pos) { return object(OBJECT_BULLET , sphere(pos, TANK_SIZE * mix(0.11, 1.11, bullet_size))); }




//------ scene
object scene(vec3 pos)
{
	float oz = pos.z;
	vec3 jade_pos = mod(pos - vec3( 1, 0, 0), vec3(4, 2, 4)) - vec3(4, 2, 4)/2;
	vec3 pltc_pos = mod(pos - vec3(-1, 0, 0), vec3(4, 2.2, 4)) - vec3(4, 2.2, 4)/2;
	jade_pos.z = oz;
	pltc_pos.z = oz;
	object grnd = obj_ground (pos - vec3(0, -1, 0));
	object jade = obj_jade   (jade_pos);
	object pltc = obj_plastic(pltc_pos);
	object plyr = obj_player (pos - player_pos);
	object bult = obj_bullet (pos - bullet_pos);
	return objects(grnd, jade, pltc, plyr, bult);
}




//------ render
object hit;
vec3 hit_point;			// valid only when not hit the sky
float dist_to_hit;		// valid only when not hit the sky
float clearness;

#define RENDER_MAX_STEP			500
#define RENDER_RADIUS			180.0
void render()
{
	float dist = 0;
	for (int i=0; i<RENDER_MAX_STEP && dist < RENDER_RADIUS; i++) {
		vec3 p = camera_pos + dist*ndir;
		object nearest = scene(p);
		if (nearest.dist < small) {
			hit = nearest;
			hit_point = p;
			dist_to_hit = dist;
			clearness = (1 - dist/RENDER_RADIUS) * (1 - i/RENDER_MAX_STEP);
			return;
		}
		dist += nearest.dist;
	}
	hit = obj_sky();
}




//------ shading
vec3 diffuse;
float thickness;

#define THICKNESS_MAX_STEP		50
#define MAX_THICKNESS			1
void populate_thickness()
{
	float dist = small*10;
	for (int i=0; i<THICKNESS_MAX_STEP && dist < MAX_THICKNESS; i++) {
		object nearest = scene(hit_point + dist*ndir);
		if (nearest.dist > 0 || -nearest.dist < small) break;
		dist -= nearest.dist;
	}
	thickness = clamp(dist / MAX_THICKNESS, 0, 1);
}

void populate_sky_diffuse()
{
	float a = dot(ndir, sun_dir);
	a = smoothstep(1 - sun_size, 1, a);
	if (ndir.y < 0) a = 0;
	sky_diffuse = mix(sky_color, sun_color, a);
}

vec3 diffuse_sky() { return sky_diffuse; }

vec3 diffuse_ground()
{
	vec2 m = mod(hit_point.xz, 2) - 1;
	float a = float(m.x * m.y > 0) * 0.5;
	return vec3(a);
}

vec3 diffuse_jade(vec3 base_color)
{
	float boost = mix(1.5, 1, smoothstep(0, 1, thickness));
	float through = mix(0.1, 0.02, smoothstep(0, 1, thickness));
	return mix(base_color * boost, sky_diffuse, through);
}

vec3 diffuse_plastic() { return vec3(0.2, 0.6, 0.1) / 1.2; }
vec3 diffuse_bullet()
{
	float brightness = lerp(sin(time * 2.3), -1, 1, 1, 1.5);
	vec3 base_color = vec3(1 * lerp(sin(2*time), -1, 1, 0.9, 1.1), 0.922 * lerp(cos(3*time), -1, 1, 0.9, 1.1),
					0.231 * lerp(sin(5*time)*cos(3*time), -1, 1, 0.9, 1.1)) ;
	if (bullet_age < 0.4) brightness *= lerp(bullet_age, 0, 0.4, 1, 0.8);
	else if (bullet_age < 0.9) brightness *= lerp(bullet_age, 0.4, 0.9, 0.8, 0.1);
	//else base_color = mix(base_color, vec3(1, 0.2, 0.1) / 10, lerp(bullet_age, 0.9, 1, 0, 1));
	else base_color = vec3(1, 0.2, 0.1);

	return base_color * brightness;
}

void populate_diffuse()
{
	switch (hit.id) {
		case OBJECT_SKY    : diffuse = diffuse_sky()    ; break;
		case OBJECT_GROUND : diffuse = diffuse_ground() ; break;
		case OBJECT_JADE   : diffuse = diffuse_jade(vec3(0.2, 0.6, 0.1)/2)   ; break;
		case OBJECT_PLASTIC: diffuse = diffuse_plastic(); break;
		case OBJECT_BULLET : diffuse = diffuse_bullet() ; break;
	}
}


vec3 normal;
void populate_normal()
{
	normal.x = scene(hit_point + vec3(small, 0, 0)).dist - scene(hit_point - vec3(small, 0, 0)).dist;
	normal.y = scene(hit_point + vec3(0, small, 0)).dist - scene(hit_point - vec3(0, small, 0)).dist;
	normal.z = scene(hit_point + vec3(0, 0, small)).dist - scene(hit_point - vec3(0, 0, small)).dist;
	normal = normalize(normal);
}


void populate_player_diffuse()
{
	diffuse = diffuse_jade(vec3(0.5, 0.2, 1.4));
	//diffuse = vec3(0.3);
	diffuse += lerp(dot(ndir, normal), -1, 0, 0, 0.4);
}



float shadow_darkness;
#define SHADOW_SHARPNESS	16
#define SHADOW_MAX_STEP		60
#define SHADOW_INTENSITY	0.4	
#define SHADOW_OFFSET		10*small
void populate_shadow()
{
	shadow_darkness = 1.0;
	float dist = 0;
	vec3 p = hit_point + normal*SHADOW_OFFSET;
	for (int i=0; i<SHADOW_MAX_STEP && dist<RENDER_RADIUS; i++) {
		object nearest = scene(p+dist*sun_dir);
		if (nearest.dist < small) {
			shadow_darkness = 0;
			break;
		}
		shadow_darkness = min(shadow_darkness, SHADOW_SHARPNESS * nearest.dist / dist);
		dist += nearest.dist;
	}
	shadow_darkness = mix(1, shadow_darkness, SHADOW_INTENSITY);
}


float ao;
#define AO_HARDNESS		4
#define AO_AMOUNT		1
void populate_ao()
{
	float step = 10*small;
	float dist = step;
	ao = 0.0;
	for (int i=0; i<AO_HARDNESS; i++) {
		float nearest = scene(hit_point + dist*normal).dist;
		ao += dist - nearest;
		dist += step;
	}
	ao = clamp(ao, 0, 1);
	ao = mix(1, 1-ao, AO_AMOUNT);
}


void light()
{
	diffuse *= mix(1, clamp(dot(normal, sun_dir), 0, 0.5) + 0.5, night_to_dawn);
	diffuse += night_to_dawn * sun_color * smoothstep(0.9, 1, dot(reflect(ndir, normal), sun_dir))*0.2;
	diffuse *= sky_color;
}

void shadow()
{
	diffuse *= mix(1.0, shadow_darkness, night_to_dawn);
}

void ambient_occlusion()
{
	diffuse *= min(1, ao + mix(0.2, 0.0, night_to_dawn));
}


void fog() { diffuse = mix(diffuse, sky_color, smoothstep(0.4, 1, 1 - clearness)); }

void shade()
{
	populate_sky_diffuse();
	if (hit.id == OBJECT_JADE || hit.id == OBJECT_PLAYER) populate_thickness();
	populate_diffuse();
	if (hit.id == OBJECT_SKY) return;
	populate_normal();
	if (hit.id == OBJECT_PLAYER) populate_player_diffuse();
	populate_shadow();
	populate_ao();

	if (hit.id != OBJECT_BULLET) {
		light();
		shadow();
	}
	ambient_occlusion();
	fog();
}




//------ animate
void animate()
{
	float brightness;
	if (sun_dir.y > 0) brightness = cos(time*3)*0.05 + lerp(sun_dir.y, 0, 1, 2.5, 3);
	else brightness = mix(1, 2.5, max(sun_dir.y, -0.3) / 0.3 + 1);
	sun_color = SUN_COLOR*brightness;
	sky_color = mix(SKY_TWILIGHT_COLOR, SKY_COLOR, max(sun_dir.y, 0));
	sky_color = mix(SKY_NIGHT_COLOR, sky_color, night_to_dawn);
	sky_color *= brightness;
	sun_size = (cos(time*2) + 10) * small/2;
}





//------ main
void main()
{
	animate();
	render();
	shade();
	color = diffuse;

	// night effect
	color.r = pow(color.r, lerp(night_to_dawn, 3, 1));
	color.g = pow(color.g, lerp(night_to_dawn, 2.5, 1));
}


